package com.example.javadocsearcher.common;

/**
 * Created with IntelliJ IDEA 2023.
 * Description:
 * User: homechen
 * Date: 2024-03-05
 * Time: 22:54
 */

import com.example.javadocsearcher.entity.DocInfo;
import com.example.javadocsearcher.entity.Result;
import com.example.javadocsearcher.entity.Weight;
import com.example.javadocsearcher.util.Config;
import com.example.javadocsearcher.util.Index;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;

/**
 * 通过用户输入的查询词，在倒排索引 + 正排索引中查找，排序，封装等
 */
@Component
public class DocSearcher {
    private Index index = new Index();

    // 处理暂停词
//    private static String STOP_WORD_PATH = "D:\\edge install\\stop_word.txt";

    private static String STOP_WORD_PATH = null;

    static {
        if (Config.isOnline) {
            STOP_WORD_PATH = "/home/tc/searcher_doc/stop_word.txt";
        } else {
            STOP_WORD_PATH = "D:\\edge install\\stop_word.txt";
        }
    }

    private HashSet<String> stopWordSet = new HashSet<>(); // 用来存储加载到内存中的暂停词

    public DocSearcher() {
        // 将索引内容加载到内存中
        index.load();
        loadStopWord();
    }

    // 静态内部类， 用于表示多路归并下一个 weight 的位置
    static class Pos {
        private int row;
        private int col;

        public Pos(int row, int col) {
            this.row = row;
            this.col = col;
        }
    }

    public void loadStopWord() {
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(STOP_WORD_PATH))) {
            while (true) {
                String stop_word = bufferedReader.readLine();
                if (stop_word == null) {
                    break;
                }
                stopWordSet.add(stop_word);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 针对 array | list 这样搜索出来的词，不应该合起来搜索出来的结果是相同的，如果一个文档中
     * 包含两个一起的内容，应该提高权重，将两个查询词的权重加起来，即实现 多路归并
     */

    public List<Result> searcher(String query) {
        // 1、分词 - 用户输入的也可能是一个词，也有可能是一段话，所以要对用户输入的内容进行分词
        List<Term> oldterms = ToAnalysis.parse(query).getTerms();
        // 处理一下查询词中的暂停词
        List<Term> terms = new ArrayList<>();
        for (Term term : oldterms) {
            if (stopWordSet.contains(term.getName())) {
                continue; // 此时查询词中存在暂停词
            }
            terms.add(term);
        }
        // 2、触发 - 根据词去查倒排索引   -  将每个词搜索出来的结果，使用二维数据进行存储，再进行合并，跟合并两个链表一致，但这里是多路的
        //  List<Weight> allTermList = new ArrayList<>();   //用于保存所有查询的倒排索引文档信息
        List<List<Weight>> termResult = new ArrayList<>();
        for (Term term : terms) {
            String word = term.getName();
            List<Weight> invertedList = index.getInverted(word); // 根据这个词去获取倒排索引拉链
            if (invertedList == null) {
                continue;
            } else {
                // 不为空，直接将这个词的倒排索引拉链添加到所有文档信息中
                termResult.add(invertedList);  // 将每个查询的结果，单独插入
            }
        }
        // 这里就是得到多路归并的结果
        List<Weight> allTermList = mergeResult(termResult);
        // 3、排序 - 根据词的权重进行降序排序  多个词就根据权重最高的进行返回
        allTermList.sort(new Comparator<Weight>() {
            @Override
            public int compare(Weight o1, Weight o2) {
                return o2.getWeight() - o1.getWeight();
            }
        });
        // 4、封装 - 根据查询到的倒排索引信息排序后的结果，去正排索引中查找文档信息，并进行封装
        List<Result> resultList = new ArrayList<>();
        for (Weight weight : allTermList) {
            //  先获取正排索引信息
            DocInfo docInfo = index.getDocInfo(weight.getDocId());
            Result result = new Result();
            result.setTitle(docInfo.getTitle());
            result.setUrl(docInfo.getUrl());
            result.setDec(GenDec(docInfo.getContent(), terms)); // 根据正文内容获取一段描述
            resultList.add(result);
        }
        return resultList;
    }

    private List<Weight> mergeResult(List<List<Weight>> source) {
        // 先针对每个查询词出来的倒排拉链，进行按照 docID 进行升序排序
        for (List<Weight> curRow : source) {
            curRow.sort(new Comparator<Weight>() {
                @Override
                public int compare(Weight o1, Weight o2) {
                    return o1.getDocId() - o2.getDocId();
                }
            });
        }

        // 再使用 优先级队列，对所有的一维数组进行排序，并去重操作，优先级队列的规则也是使用 docID 的规则进行比较
        List<Weight> target = new ArrayList<>();
        PriorityQueue<Pos> queue = new PriorityQueue<>(new Comparator<Pos>() {
            @Override
            public int compare(Pos o1, Pos o2) {
                // 先获取到对应 pos 位置的 weight 对象
                Weight weight1 = source.get(o1.row).get(o1.col);
                Weight weight2 = source.get(o2.row).get(o2.col);
                // 按照 docId 升序的规则进行建堆
                return weight1.getDocId() - weight2.getDocId();
            }
        });

        // 将所有查询词列表的首个 weight 进行插入到优先级队列中
        for (int row = 0; row < source.size(); row++) {
            queue.offer(new Pos(row, 0));
        }

        // 循环的取出队列首个元素
        while (!queue.isEmpty()) {
            Pos minPos = queue.poll();
            // 获取这个位置的 weight 对象
            Weight weight = source.get(minPos.row).get(minPos.col);
            // 跟插入到结果链表的前一个元素进行对比，如果相同，就不插入到结果列表，并将权重进行相加到前一个元素中
            if (target.size() > 0) {
                Weight weightLast = target.get(target.size() - 1);
                // 如果 docId 相同就合并权重
                if (weightLast.getDocId() == weight.getDocId()) {
                    int totalWeight = weightLast.getWeight() + weight.getWeight();
                    weightLast.setWeight(totalWeight);
                } else {
                    // 此时 docId 不同就插入到结果链表中
                    target.add(weight);
                }
            } else {
                // 此时说明是第一次在结果链表中插入元素
                target.add(weight);
            }

            // 当最小的 pos 处理完毕后，就需要将，光标在这一行，向后移动一个
            Pos newPos = new Pos(minPos.row, minPos.col + 1);
            // 判断走的位置是否合法，如果合法，就讲 newPos 入队列
            if (newPos.col >= source.get(newPos.row).size()) {
                // 如果这一行走到了末尾了，就结束
                continue;
            }
            queue.offer(newPos);
        }
        return target;
    }

    //  因为倒排索引到结果是 标题 + 正文的合，所以这里需要遍历所有的词，查看是否在正文中存在
    private String GenDec(String content, List<Term> terms) {
        int firstPos = -1; // 用于定义正文中查询的位置
        for (Term term : terms) {
            String word = term.getName();
            // 这里的正文部分，少考虑了， array)  | array. 这种情况，使用正则表达式去掉，这种情况
            content = content.toLowerCase().replaceAll("\\b" + word + "\\b", " " + word + " ");
            firstPos = content.indexOf(" " + word + " "); // 这里要按照全词匹配，比如：老婆  ！-》老婆饼 因为这里将所有的词，进行转小写，所以这里需要将正文转为小写
            if (firstPos >= 0) {
                // 成功在正文中找到匹配词  找到一个就退出
                break;
            }
        }

        if (firstPos == -1) {
            // 此时说明没有找到一个词，只是标题中含有匹配的词，直接从正文开始进行截取
            if (content.length() > 160) {
                return content.substring(0, 160) + "...";
            }
            return content;
        }

        // 以 firstPos 为基准，向前找60位置为起点，向后截取 160 个字符
        String desc = "";
        int descBegin = firstPos > 60 ? firstPos - 60 : 0;
        if (descBegin + 160 > content.length()) {
            // 此时说明，后面没有160 个字符了，直接截取完
            desc = content.substring(descBegin);
        } else {
            desc = content.substring(descBegin, descBegin + 160) + "..."; // begin，end
        }

        // 处理前端搜索关键词时，描述标红操作  遍历搜索词，将描述中的部分 加上 <i> 标签  斜体
        for (Term term : terms) {
            String word = term.getName();
            // (?!)  表示匹配忽略大小写
            desc = desc.replaceAll("(?i)" + " " + word + " ", " <i>" + word + "</i> ");
        }
        return desc;
    }

    // 测试发现 还有 JS 代码存在，需要使用正则表达式进行去除
//    public static void main(String[] args) {
//        DocSearcher docSearcher = new DocSearcher();
//        Scanner scanner = new Scanner(System.in);
//        while (true) {
//            System.out.print("请输入查询词：");
//            String query = scanner.next();
//            List<Result> list = docSearcher.searcher(query);
//            for (Result result : list) {
//                System.out.println("=============================");
//                System.out.println(result);
//            }
//        }
//    }

}
